package com.blink.jtblc.core.types.known.tx.result;

import java.util.HashMap;
import java.util.Map;

import org.json.JSONObject;

import com.blink.jtblc.core.coretypes.AccountID;
import com.blink.jtblc.core.coretypes.STArray;
import com.blink.jtblc.core.coretypes.STObject;
import com.blink.jtblc.core.coretypes.hash.Hash256;
import com.blink.jtblc.core.coretypes.hash.Index;
import com.blink.jtblc.core.coretypes.uint.UInt32;
import com.blink.jtblc.core.fields.Field;
import com.blink.jtblc.core.serialized.enums.EngineResult;
import com.blink.jtblc.core.serialized.enums.LedgerEntryType;
import com.blink.jtblc.core.serialized.enums.TransactionType;
import com.blink.jtblc.core.types.known.tx.Transaction;
import com.blink.jtblc.encoding.common.B16;

public class TransactionResult implements Comparable<TransactionResult> {
	// The json formatting of transaction results is a MESS
	public enum Source {
		request_tx_result, request_account_tx, request_account_tx_binary, request_tx_binary, ledger_transactions_expanded_with_ledger_index_injected, transaction_subscription_notification
	}
	
	public EngineResult engineResult;
	public UInt32 ledgerIndex;
	public Hash256 hash;
	// TODO, consider just killing this field, as not all have them
	@Deprecated
	public Hash256 ledgerHash;
	// TODO, in practice this class is only for validated results so ...
	@Deprecated
	public boolean validated;
	
	public TransactionResult(long ledgerIndex, Hash256 hash, Transaction txn, TransactionMeta meta) {
		this.ledgerIndex = new UInt32(ledgerIndex);
		this.hash = hash;
		this.txn = txn;
		this.meta = meta;
		this.engineResult = meta.engineResult();
		this.validated = true;
	}
	
	public Transaction txn;
	public TransactionMeta meta;
	// This is deprecated because it's not always set.
	@Deprecated
	public JSONObject message;
	
	public boolean isPayment() {
		return transactionType() == TransactionType.Payment;
	}
	
	public TransactionType transactionType() {
		return txn.transactionType();
	}
	
	public AccountID createdAccount() {
		AccountID destination = null;
		Hash256 destinationIndex = null;
		if (transactionType() == TransactionType.Payment && meta.has(Field.AffectedNodes)) {
			STArray affected = meta.get(STArray.AffectedNodes);
			for (STObject node : affected) {
				if (node.has(STObject.CreatedNode)) {
					STObject created = node.get(STObject.CreatedNode);
					if (STObject.ledgerEntryType(created) == LedgerEntryType.AccountRoot) {
						if (destination == null) {
							destination = txn.get(AccountID.Destination);
							destinationIndex = Index.accountRoot(destination);
						}
						if (destinationIndex.equals(created.get(Hash256.LedgerIndex))) {
							return destination;
						}
					}
				}
			}
		}
		return null;
	}
	
	public Map<AccountID, STObject> modifiedRoots() {
		HashMap<AccountID, STObject> accounts = null;
		if (meta.has(Field.AffectedNodes)) {
			accounts = new HashMap<AccountID, STObject>();
			STArray affected = meta.get(STArray.AffectedNodes);
			for (STObject node : affected) {
				if (node.has(Field.ModifiedNode)) {
					node = node.get(STObject.ModifiedNode);
					if (STObject.ledgerEntryType(node) == LedgerEntryType.AccountRoot) {
						STObject finalFields = node.get(STObject.FinalFields);
						AccountID key;
						if (finalFields != null) {
							key = finalFields.get(AccountID.Account);
							accounts.put(key, node);
						}
					}
				}
			}
		}
		return accounts;
	}
	
	public AccountID initiatingAccount() {
		return txn.get(AccountID.Account);
	}
	
	@Override
	public int compareTo(TransactionResult o2) {
		TransactionResult o1 = this;
		int i = o1.ledgerIndex.compareTo(o2.ledgerIndex);
		if (i != 0) {
			return i;
		} else {
			return o1.meta.transactionIndex().compareTo(o2.meta.transactionIndex());
		}
	}
	
	public static TransactionResult fromJSON(JSONObject json) {
		boolean binary;
		String metaKey = json.has("meta") ? "meta" : "metaData";
		String txKey = json.has("transaction") ? "transaction" : json.has("tx") ? "tx" : json.has("tx_blob") ? "tx_blob" : null;
		if (txKey == null && !json.has("TransactionType")) {
			throw new RuntimeException("This json isn't a transaction " + json);
		}
		binary = txKey != null && json.get(txKey) instanceof String;
		Transaction txn;
		if (txKey == null) {
			// This should parse the `hash` field
			txn = (Transaction) STObject.fromJSONObject(json);
		} else {
			txn = (Transaction) parseObject(json, txKey, binary);
			if (json.has("hash")) {
				txn.put(Hash256.hash, Hash256.fromHex(json.getString("hash")));
			} else if (binary) {
				byte[] decode = B16.decode(json.getString(txKey));
				txn.put(Hash256.hash, Index.transactionID(decode));
			}
		}
		TransactionMeta meta = (TransactionMeta) parseObject(json, metaKey, binary);
		long ledger_index = json.optLong("ledger_index", 0);
		if (ledger_index == 0 && !binary) {
			ledger_index = json.getJSONObject(txKey).getLong("ledger_index");
		}
		TransactionResult tr = new TransactionResult(ledger_index, txn.get(Hash256.hash), txn, meta);
		if (json.has("ledger_hash")) {
			tr.ledgerHash = Hash256.fromHex(json.getString("ledger_hash"));
		}
		return tr;
	}
	
	private static STObject parseObject(JSONObject json, String key, boolean binary) {
		if (binary) {
			return STObject.translate.fromHex(json.getString(key));
		} else {
			JSONObject tx_json = json.getJSONObject(key);
			return STObject.translate.fromJSONObject(tx_json);
		}
	}
	
	public TransactionResult(JSONObject json, Source resultMessageSource) {
		message = json;
		if (resultMessageSource == Source.transaction_subscription_notification) {
			engineResult = EngineResult.valueOf(json.getString("engine_result"));
			validated = json.getBoolean("validated");
			ledgerHash = Hash256.translate.fromString(json.getString("ledger_hash"));
			ledgerIndex = new UInt32(json.getLong("ledger_index"));
			if (json.has("transaction")) {
				txn = (Transaction) STObject.fromJSONObject(json.getJSONObject("transaction"));
				hash = txn.get(Hash256.hash);
			}
			if (json.has("meta")) {
				meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("meta"));
			}
		} else if (resultMessageSource == Source.ledger_transactions_expanded_with_ledger_index_injected) {
			validated = true;
			meta = (TransactionMeta) STObject.translate.fromJSONObject(json.getJSONObject("metaData"));
			txn = (Transaction) STObject.translate.fromJSONObject(json);
			hash = txn.get(Hash256.hash);
			engineResult = meta.engineResult();
			ledgerIndex = new UInt32(json.getLong("ledger_index"));
			ledgerHash = null;
		} else if (resultMessageSource == Source.request_tx_result) {
			validated = json.optBoolean("validated", false);
			if (validated && !json.has("meta")) {
				throw new IllegalStateException("It's validated, why doesn't it have meta??");
			}
			if (validated) {
				meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("meta"));
				engineResult = meta.engineResult();
				txn = (Transaction) STObject.fromJSONObject(json);
				hash = txn.get(Hash256.hash);
				ledgerHash = null; // XXXXXX
				ledgerIndex = new UInt32(json.getLong("ledger_index"));
			}
		} else if (resultMessageSource == Source.request_account_tx) {
			validated = json.optBoolean("validated", false);
			if (validated && !json.has("meta")) {
				throw new IllegalStateException("It's validated, why doesn't it have meta??");
			}
			if (validated) {
				JSONObject tx = json.getJSONObject("tx");
				meta = (TransactionMeta) STObject.fromJSONObject(json.getJSONObject("meta"));
				engineResult = meta.engineResult();
				this.txn = (Transaction) STObject.fromJSONObject(tx);
				hash = this.txn.get(Hash256.hash);
				ledgerIndex = new UInt32(tx.getLong("ledger_index"));
				ledgerHash = null;
			}
		} else if (resultMessageSource == Source.request_account_tx_binary || resultMessageSource == Source.request_tx_binary) {
			validated = json.optBoolean("validated", false);
			if (validated && !json.has("meta")) {
				throw new IllegalStateException("It's validated, why doesn't it have meta??");
			}
			if (validated) {
				/*
				 * {
				 * "ledger_index": 3378767,
				 * "meta": "201 ...",
				 * "tx_blob": "120 ...",
				 * "validated": true
				 * },
				 */
				boolean account_tx = resultMessageSource == Source.request_account_tx_binary;
				String tx = json.getString(account_tx ? "tx_blob" : "tx");
				byte[] decodedTx = B16.decode(tx);
				meta = (TransactionMeta) STObject.translate.fromHex(json.getString("meta"));
				this.txn = (Transaction) STObject.translate.fromBytes(decodedTx);
				if (account_tx) {
					hash = Index.transactionID(decodedTx);
				} else {
					hash = Hash256.translate.fromHex(json.getString("hash"));
				}
				this.txn.put(Field.hash, hash);
				engineResult = meta.engineResult();
				ledgerIndex = new UInt32(json.getLong("ledger_index"));
				ledgerHash = null;
			}
		}
	}
	
	@Override
	public String toString() {
		// message is deprecated
		if (message != null) {
			return message.toString();
		} else {
			JSONObject object = toJSON();
			return object.toString();
		}
	}
	
	public JSONObject toJSON() {
		JSONObject o = new JSONObject();
		o.put("tx", txn.toJSON());
		o.put("meta", meta.toJSON());
		o.put("ledger_index", ledgerIndex);
		o.put("hash", hash.toHex());
		return o;
	}
	
	public TransactionResult copy() {
		TransactionMeta metaCopy = (TransactionMeta) STObject.translate.fromBytes(meta.toBytes());
		Transaction txnCopy = (Transaction) STObject.translate.fromBytes(txn.toBytes());
		return new TransactionResult(ledgerIndex.longValue(), hash, txnCopy, metaCopy);
	}
	
	public JSONObject toJSONBinary() {
		JSONObject o = new JSONObject();
		o.put("hash", hash.toHex());
		o.put("meta", meta.toHex());
		o.put("tx", txn.toHex());
		o.put("ledger_index", ledgerIndex);
		return o;
	}
}
